#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

//这样写宏函数就可以 按照自己的意愿在使用时写;了 而不用在意它是一个宏函数
//如果宏要写多行 就一定要加续行符'\' 并且续行符一定要在一行的最后
#define spaceTrim(begin) do {\
    while(isspace(*begin)) {++begin;}\
} while(0) //这里不需要写分号';' 如while(0);  这里的0是只因为想让它执行do的一次

#define USER "USER"
#define HOSTNAME "miniShell"
//C语言创建数组需要用常量 因此定义宏设定数组长度
#define COM_NUM 1024
#define MY_ARGC 128

//记录重定向的模式
enum {
    NONE_REDIRECT, //不重定向
    INPUT_REDIRECT, //输入重定向 "<"
    OUT_REDIRECT, //输出重定向 ">"
    APPEND_REDIRECT //追加重定向 ">>"
}redirType;

//用来存放用户输入的命令
char lineCommand[COM_NUM];
//存放命令行参数
char* myArgv[MY_ARGC];
//记录上一个进程退出的信号
int lastSignal = 0;
//记录上一个进程的退出码
int lastExitCode = 0;
//记录重定向的文件
char* redirFile = NULL;

void redirectCheck(char* lineCommand)
{
    assert(lineCommand);
    char* begin = lineCommand;
    //end指向字符串末尾的'\0'
    char* end = begin + strlen(lineCommand);

    while(begin < end)
    {
        //判断是否存在重定向符 存在的话又是哪种重定向符
        if (*begin == '>') //输出和追加重定向
        {  
            *begin = '\0';
            ++begin;
            if(*begin == '>') //追加重定向
            {
                redirType = APPEND_REDIRECT;

                *begin = '\0';
                ++begin;
            }
            else //输出重定向
            {
                redirType = OUT_REDIRECT;
            }

            spaceTrim(begin);
            redirFile = begin;

            return;
        }
        else if (*begin == '<') //输入重定向
        {
            redirType = INPUT_REDIRECT;

            *begin = '\0';
            ++begin;
            //去除重定向符后多余的空格 如： ls >                    test.txt
            spaceTrim(begin);
            //去除完多余的空格后 begin指向的位置就是重定向文件的名字
            redirFile = begin;

            return;
        }
        else
        {
            ++begin;
        }
    }

    redirType = NONE_REDIRECT;
    redirFile = NULL;
}

int main()
{
    while(1)
    {
        printf("[%s@%s]$ ", getenv(USER), HOSTNAME);

        //刷新输出缓冲区 把上面的Shell标识输出出来
        fflush(stdout);

        //从键盘中读取命令 这里COM_NUM - 1的目的是留一个位置存放'\0'
        char* s = fgets(lineCommand, COM_NUM - 1, stdin);

        //如果读取出现错误 fgets会返回NULL 要对读取失败的情况进行检查
        assert(s);

        //因为fgets是读取一行的数据 所以会把'\n'给读取进来 我们需要把'\n'给消除掉
        //lineCommand为char类型数组 0在其中表现为'\0'
        lineCommand[strlen(lineCommand) - 1] = 0/*'\0'*/;

        //检查输入的命令中是否含有重定向"<" ">" ">>"
        redirectCheck(lineCommand);

        //将读取的命名分割成一个一个的命令行参数 如将 "ls -a -l -d"分割成 "ls" "-a" "-l" "-d"
        int count = 0;
        //strtok用于切割字符串 切割成功会返回指向被分割出来的字符串的指针 切割失败会返回空指针
        myArgv[count++] = strtok(lineCommand, " ");
        //如果读取到的命令为ls就补上--color=auto命令 用于给ls出来的文件显示颜色 更符合原本的Shell
        if(myArgv[0] != NULL && strcmp("ls", myArgv[0]) == 0)
        {
            myArgv[count++] = "--color=auto";
        }

        //切割剩下的字符串 strtok在切割同一个字符串时 
        //只有第一次切割需要传入指向该字符串的指针 以后再切割时传入空指针就可以了
        //下面的语句不仅可以切割剩下的字符串 还可以在切割完毕后补上一个NULL 因为在没有字符串可以切割时 strtok就会返回NULL
        //恰好在传入命令行参数时就需要以NULL结尾
        while(myArgv[count++] = strtok(NULL, " "));

        //cd是一个内建命令 即不是由Shell产生的子进程执行的 而是由Shell本身执行的
        if(myArgv[0] != NULL && strcmp("cd", myArgv[0]) == 0)
        {
            if(myArgv[1] != NULL)
            {
                //更改进程的运行目录 运行目录也叫当前目录 pwd即是显示当前目录
                chdir(myArgv[1]);
                //cd本身就是一条指令 执行完之后就可以直接去接收下一条指令了
                //这里是continue跳过本次循环后面的语句 而不是break跳出整个循环
                continue;
            }
        }

        //echo也是一个内建命令
        if(myArgv[0] != NULL && strcmp("echo", myArgv[0]) == 0)
        {
            //echo $?命令会输出上一个已退出进程的退出码
            if(myArgv[1] != NULL && strcmp(myArgv[1], "$?") == 0)
            {
                //这里我还额外输出了上一个已退出进程的信号
                printf("signal: %d, exitCode: %d\n", lastSignal, lastExitCode);
            }
            else
            {
                int i = 1;
                while(myArgv[i] != NULL)
                {
                    printf("%s ", myArgv[i]);
                    ++i;
                }

                printf("\n");
            }
            continue;
        }

        pid_t id = fork();
        //fork创建子进程失败会返回-1
        assert(id != -1);

        //fork创建子进程成功 子进程得到的返回值为0
        if(id == 0)
        {
            switch (redirType)
            {
                case NONE_REDIRECT: //啥也不干
                break;
                case INPUT_REDIRECT:
                {
                    int fd = open(redirFile, O_RDONLY);
                    if (fd == -1)
                    {
                        perror("open");
                        exit(errno);
                    }

                    //stdin的fd为0
                    int dup2Ret = dup2(fd, 0);
                    if (dup2Ret == -1)
                    {
                        perror("dup2");
                        exit(errno);
                    }
                }
                break;
                //输入重定向和输出重定向打开文件的方式差不多 就一起处理
                case OUT_REDIRECT:
                case APPEND_REDIRECT:
                {
                    int flags = O_WRONLY | O_CREAT;
                    flags = redirType == OUT_REDIRECT ? 
                            flags | O_TRUNC : 
                            flags | O_APPEND;
                    int fd = open(redirFile, flags, 0666);
                    if (fd == -1)
                    {
                        perror("open");
                        exit(errno);
                    }

                    //stdout的fd为1 stderror的fd为2
                    int dup2Ret = dup2(fd, 1);
                    if (dup2Ret == -1)
                    {
                        perror("dup2");
                        exit(errno);
                    }
                }
                break;
                default:
                {
                    printf("Need Debug!!!\n");
                    exit(-1);
                }
                break;
            }
            //对子进程进行进程替换 
            //让子进程来执行命令然后退出 而不是让父进程(Shell)来执行命令
            execvp(myArgv[0], myArgv);
            //在进行进程替换之后 该进程就不会再执行后面的语句
        }

        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);

        lastSignal = status & 0X7F;
        lastExitCode = (status >> 8) & 0XFF;
    }
    return 0;
}